package com.jeasonzhao.report.engine.repo;

import java.sql.SQLException;
import java.util.Hashtable;
import java.util.Vector;

import com.jeasonzhao.commons.logger.Logger;
import com.jeasonzhao.commons.utils.Algorithms;
import com.jeasonzhao.commons.utils.DataTypes;
import com.jeasonzhao.report.codes.ConfigSrcTypes;
import com.jeasonzhao.report.codes.ReportDictTypes;
import com.jeasonzhao.report.engine.Configuration;
import com.jeasonzhao.report.engine.irp.IDictItemsProvider;
import com.jeasonzhao.report.exceptions.DatabaseException;
import com.jeasonzhao.report.exceptions.DictException;
import com.jeasonzhao.report.model.DictItem;
import com.jeasonzhao.report.model.DictItemCollection;
import com.jeasonzhao.report.model.ObjectCollection;
import com.jeasonzhao.report.model.ReportDatabaseConfiguration;
import com.jeasonzhao.report.model.ReportDict;
import com.jeasonzhao.report.msg.MessageCodes;
import com.jeasonzhao.report.engine.DatabasePool;
import com.jeasonzhao.commons.db.Sqlca;

public class DictRepository
{
    private Hashtable<String,DictItemCollection> m_dictItemsCache = new Hashtable<String,DictItemCollection>();
    private Hashtable<String,ReportDict> m_dictConfigurations = new Hashtable<String,ReportDict>();
    private Hashtable<String,IDictItemsProvider> m_dictItemsProvider = new Hashtable<String,IDictItemsProvider>();
    private static volatile DictRepository m_instance = null;
    private DictRepository()
    {
    }

    public static DictRepository getInstance()
    {
        if(null == m_instance)
        {
            synchronized(DictRepository.class)
            {
                m_instance = new DictRepository();
            }
        }
        return m_instance;
    }

    private String alignId(String str)
    {
        return Algorithms.isEmpty(str) ? null : str.trim().toLowerCase();
    }

    public void addDictionaryConfiguration(ReportDict dict)
        throws DictException
    {
        if(null == dict)
        {
            return;
        }
        if(Algorithms.isEmpty(dict.getId()))
        {
            throw new DictException.NoIdentifier(dict);
        }
        String strDictId = validateDictConfig(dict);
        Configuration.getInstance().getLogger().info(MessageCodes.format(
            MessageCodes.CFG_ADDDICTIONARY
            ,MessageCodes.getLongDescription(dict)));

        m_dictConfigurations.put(strDictId,dict);
        if(null != dict.getItems())
        {
            m_dictItemsCache.put(strDictId,dict.getItems());
        }
        if(dict.getDictType().equals(ReportDictTypes.SQLTree))
        {
            //For SQLTree, it should be initialized before use it, because it
            //will introduce new dictionaries into the repository
            //And further more, the repository does not know how to load the datas
            //for those new dictionaries.
            this.prepareItemsForSqlDict(dict);
        }
    }

    private String validateDictConfig(ReportDict dict)
        throws DictException
    {
        String strDictId = this.alignId(dict.getId());
        if(dict.getDictType().equals(ReportDictTypes.SQL)
           || dict.getDictType().equals(ReportDictTypes.SQLTree))
        {
            if(Algorithms.isEmpty(dict.getDatabaseId()))
            {
                throw new DictException.NoDatabase(dict);
            }
            if(Algorithms.isEmpty(dict.getSqlStatement()))
            {
                throw new DictException.NoSQL(dict);
            }
            if(dict.getDictType().equals(ReportDictTypes.SQLTree)
               && Algorithms.isEmpty(dict.getParameter()))
            {
                throw new DictException.NoTreeDefined(dict);
            }
        }
        else if(dict.getDictType().equals(ReportDictTypes.External))
        {
            if(Algorithms.isEmpty(dict.getParameter()))
            {
                throw new DictException.NoClassForExternalDict(dict);
            }
            String strClassName = dict.getParameter().trim();
            try
            {
                Class<?> cls = Class.forName(strClassName);
                if(IDictItemsProvider.class.isAssignableFrom(cls) == false)
                {
                    throw new DictException.CastError(dict,strClassName,IDictItemsProvider.class);
                }
                IDictItemsProvider p = (IDictItemsProvider) cls.newInstance();
                this.m_dictItemsProvider.put(strDictId,p);
            }
            catch(IllegalAccessException ex)
            {
                throw new DictException.CastError(dict,strClassName,IDictItemsProvider.class,ex);
            }
            catch(InstantiationException ex)
            {
                throw new DictException.CastError(dict,strClassName,IDictItemsProvider.class,ex);
            }
            catch(ClassNotFoundException ex)
            {
                throw new DictException.CastError(dict,strClassName,IDictItemsProvider.class,ex);
            }
        }
        else if(dict.getDictType().equals(ReportDictTypes.Custom))
        {
            if(null == dict.getItems() || dict.getItems().size() < 1)
            {
                throw new DictException.NoData(dict);
            }
        }
        else
        {
            throw new DictException.UnknownType(dict,dict.getDictType());
        }
        if(Algorithms.isEmpty(dict.getName()))
        {
            dict.setName(dict.getId());
        }
        return strDictId;
    }

    public ReportDict[] getParentDictDefines(String strId)
    {
        if(null != strId && strId.startsWith("@"))
        {
            strId = strId.substring(1);
        }
        Vector<ReportDict> vect = new Vector<ReportDict>();
        ReportDict dict = this.getDictionaryConfiguration(strId);
        if(dict != null)
        {
            vect.add(dict);
        }
        while(dict != null && dict.getParentId() != null)
        {
            dict = this.getDictionaryConfiguration(dict.getParentId());
            vect.add(dict);
        }
        ReportDict[] ret = new ReportDict[vect.size()];
        vect.copyInto(ret);
        return ret;
    }

    public String jsonNamesOf(String strDictId,String strx)
    {
        if(null == strDictId || null == strx || strx.startsWith("="))
        {
            return strx;
        }
        if(strx.startsWith("["))
        {
            strx = strx.substring(1);
        }
        if(strx.endsWith("]"))
        {
            strx = strx.substring(0,strx.length() - 1);
        }
        String[] arg = strx.split(",");
        StringBuffer strName = new StringBuffer();
        if(arg.length > 1)
        {
            strName.append('[');
        }
        for(int n = 0;n < arg.length;n++)
        {
            if(strName.length() > (arg.length > 1 ? 1 : 0))
            {
                strName.append(',');
            }
            try
            {
                strName.append(itemOf(strDictId,arg[n]).toString());
            }
            catch(Exception excep)
            {
                strName.append(arg[n]);
            }
        }
        if(arg.length > 1)
        {
            strName.append(']');
        }
        return strName.toString();
    }

    public DictItem itemOf(String strDictId,Object key)
        throws DictException
    {
        if(Algorithms.isEmpty(strDictId) || Algorithms.isEmpty(key))
        {
            return null;
        }
        ReportDict[] configs = null;
        if(strDictId.trim().startsWith("@"))
        {
            strDictId = strDictId.substring(1);
            configs = this.getParentDictDefines(strDictId);
        }
        else
        {
            ReportDict dict = this.getDictionaryConfiguration(strDictId);
            if(null != dict)
            {
                configs = new ReportDict[]
                    {dict};
            }
        }
        if(null == configs || configs.length < 1)
        {
            return null;
        }
        try
        {
            key = configs[configs.length - 1].getKeyDataType().castValue(key);
        }
        catch(Exception exceo)
        {
        }
        for(int n = 0;null != configs && n < configs.length;n++)
        {
            if(null == configs[n])
            {
                continue;
            }
            DictItemCollection collection = this.items(configs[n].getId());
            DictItem item = null == collection ? null : collection.getItemFromID(key);
            if(null != item)
            {
                item.setCurrentLevel(n);
                return item;
            }
        }
        return null;
    }

    public String nameOf(String strDictId,Object key)
    {
        try
        {
            DictItem item = this.itemOf(strDictId,key);
            return null == item ? Algorithms.toString(key) : item.getName();
        }
        catch(DictException ex)
        {
            return Algorithms.toString(key);
        }
    }

    public DictItemCollection items(String strDictId)
        throws DictException
    {
        strDictId = this.alignId(strDictId);
        if(null == strDictId)
        {
            return null;
        }
        if(m_dictItemsCache.containsKey(strDictId))
        {
            return m_dictItemsCache.get(strDictId);
        }
        ReportDict dict = this.getDictionaryConfiguration(strDictId);
        if(null == dict)
        {
            return null;
        }
        if(dict.getDictType().equals(ReportDictTypes.External))
        {
            return prepareItemsForExternalDict(dict);
        }
        else
        {
            prepareItemsForSqlDict(dict);
            return m_dictItemsCache.get(strDictId);
        }

    }

    public java.util.ArrayList<ReportDict> getAllConfigurations()
    {
        java.util.ArrayList<ReportDict> ret = new java.util.ArrayList<ReportDict>();
        for(java.util.Iterator<ReportDict> it = m_dictConfigurations.values().iterator();it.hasNext();)
        {
            ret.add(it.next());
        }
        return ret;
    }

    public ReportDict getDictionaryConfiguration(String strDictId)
    {
        strDictId = this.alignId(strDictId);
        if(null == strDictId)
        {
            return null;
        }
        return m_dictConfigurations.get(strDictId);
    }
    private static final String ID_COLUMNNAME="ID";
    private static final String NAME_COLUMNNAME="NAME";
    private static final String PARENTID_COLUMNNAME="PARENTID";
    private void prepareItemsForSqlDict(ReportDict dict)
        throws DictException
    {
        if(null == dict ||
           (false == dict.getDictType().equals(ReportDictTypes.SQL)
            && false == dict.getDictType().equals(ReportDictTypes.SQLTree)))
        {
            return;
        }
        Sqlca sqlagent = null;
        try
        {
            dict.setDictItems(null);
            String strDictId = alignId(dict.getId());
            DictItemCollection itemCollections = new DictItemCollection(dict.getKeyDataType());
            String strSql = dict.getSqlStatement();
            ReportDatabaseConfiguration dsc = DatabaseConfigurationRepository.getInstance().get(dict.getDatabaseId());
            if(null == dsc)
            {
                throw new DictException.NoDatabase(dict);
            }
            sqlagent = DatabasePool.getInstance().getSqlca(dsc.getDatabaseId());
            sqlagent.execute(strSql);


            int ncolumnsCount=sqlagent.getColumnCount();
            if(ncolumnsCount<2)
            {
                throw new DictException.SQLColumnNotEnough(dict);
            }
            int idi = sqlagent.indexOfColumn(ID_COLUMNNAME);
            int idn = sqlagent.indexOfColumn(NAME_COLUMNNAME);
            int idp = sqlagent.indexOfColumn(PARENTID_COLUMNNAME);
            if(idi < 1 || idn< 1)//JDBC style array indexes.
            {
                if(ncolumnsCount == 2)
                {
                    idp = -1;
                    idi = 1;
                    idn = 2;
                }
                else
                {
                    idp = 1;
                    idi = 2;
                    idn = 3;
                }
            }
            while(sqlagent.next())
            {
                Object pid=idp<1?null:dict.getKeyDataType().castValue(sqlagent.get(idp));
                Object id=dict.getKeyDataType().castValue(sqlagent.get(idi));
                String name=sqlagent.getString(idn);
                if(Algorithms.isEmpty(id) && Configuration.getInstance().getTriggers().isAutoRemoveEmptyDictItems())
                {
                        continue;
                }
                if(Algorithms.isEmpty(name))
                {
                    name=id.toString();
                }
                itemCollections.addDictItem(pid,id,name);
            }

            if(dict.getDictType().equals(ReportDictTypes.SQL))
            {
                this.m_dictItemsCache.put(strDictId,itemCollections);
            }
            else if(dict.getDictType().equals(ReportDictTypes.SQLTree))
            {
//                log.debug("Expanding dictionary to make categories for " + dict + "...");
                String[] strAppend = dict.getParameter().split("\\|");
                String strRootNode = strAppend[0].trim();
                if(strRootNode.equalsIgnoreCase("[null]") || strRootNode.equalsIgnoreCase("#null#"))
                {
                    strRootNode = null;
                }
                ObjectCollection pks = new ObjectCollection();
                String strParentDictId = dict.getParentId();
                for(int n = 0;n < strAppend.length;n++)
                {
                    DictItemCollection leve = new DictItemCollection(dict.getKeyDataType());
                    String[] args = strAppend[n].split("\\:");
                    String strSubDictId = args[0].trim();
                    String strSubDictName = args.length < 2 ? dict.getName() + "Level" + n : args[1];
//                    log.debug("Initializing category " + dict + " > " + strSubDictName + "[" + strSubDictId + "]....");
                    if(n == 0)
                    {
                        strSubDictId = dict.getId();
                        strSubDictName = dict.getName();
                        leve.addAll(itemCollections.getItemFromParentID(strRootNode));
                    }
                    else
                    {
                        for(int x = 0;x < pks.size();x++)
                        {
                            leve.addAll(itemCollections.getItemFromParentID(pks.elementAt(x)));
                        }
                    }
                    if(leve.size() < 1)
                    {
                        createCustomDict(strParentDictId,strSubDictId,strSubDictName,dict.getKeyDataType(),itemCollections,dict.getConfigType(),dict.getConfigSrc());
                        break;
                    }
                    createCustomDict(strParentDictId,strSubDictId,strSubDictName,dict.getKeyDataType(),leve,dict.getConfigType(),"@" + dict.getConfigSrc());
                    strParentDictId = strSubDictId;
                    pks = new ObjectCollection();
                    for(int x = 0;x < leve.size();x++)
                    {
                        pks.add(leve.elementAt(x).getKey());
                    }
                }
            }
        }
        catch(DictException ex)
        {
            throw ex;
        }
        catch(SQLException ex)
        {
            throw new DictException.SQLExecutingError(dict,ex);
        }
        catch(DatabaseException ex)
        {
            throw new DictException.SQLExecutingError(dict,ex);
        }
        finally
        {
            if(null != sqlagent)
            {
                sqlagent.closeAll();
            }
        }
    }

    private DictItemCollection prepareItemsForExternalDict(ReportDict dict)
        throws DictException
    {
        if(null == dict || dict.getDictType().equals(ReportDictTypes.External) == false)
        {
            return null;
        }
        Logger log = Configuration.getInstance().getLogger();
        try
        {
            String strDictId = this.alignId(dict.getId());
            DictItemCollection coll = m_dictItemsCache.get(strDictId);
            IDictItemsProvider p = this.m_dictItemsProvider.get(strDictId);
            if(p == null)
            {
                log.error("Dictionary " + dict + " initializing error: could not found the provider from class " + dict.getParameter());
                return coll;
            }
            if(p.isTemporary())
            {
                //Never save items into the cache.
                coll = p.getDictItems(dict);
                m_dictItemsProvider.remove(strDictId);
                return coll;
            }
            else
            {
                //Check whether the dict has been generated value
                if(null != coll && coll.size() > 0)
                {
                    return coll;
                }
                else
                {
                    coll = p.getDictItems(dict);
                    this.m_dictItemsCache.put(strDictId,coll);
                    return coll;
                }
            }
        }
        catch(Exception ex)
        {
            log.error("Initializing dictionary items from class " + dict.getParameter() + " failed",ex);
            throw new DictException.ExternalExecutingError(dict,ex);
        }
    }

    private void createCustomDict(String strParentId,String strId,String strName
                                  ,DataTypes dataType,DictItemCollection items,ConfigSrcTypes t,String strConfigSrc)
        throws DictException
    {
        if(null == strId || null == items || strId.trim().length() < 1)
        {
            return;
        }
        strId = strId.trim().toLowerCase();
        ReportDict dict = new ReportDict();
        dict.setDictType(ReportDictTypes.Custom);
        dict.setId(alignId(strId));
        dict.setParentId(alignId(strParentId));
        dict.setKeyDataType(dataType);
        dict.setName(strName);
        dict.setDictItems(items);
        dict.setConfigType(t);
        dict.setConfigSrc(strConfigSrc);
        this.addDictionaryConfiguration(dict);
    }
}
